package org.yes.util;

import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.yes.anotations.*;
import org.yes.enumerable.When;
import org.yes.util.helper.HeaderInfo;
import org.yes.util.helper.Notifiers;
import org.yes.util.helper.ConversionInfo;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.*;
import java.math.BigDecimal;

/**
 * User: mtymes
 * Date: May 27, 2010
 * Time: 12:15:48 AM
 */
public class ExtractionUtil {


    /* ====================== */
    /* --- public methods --- */
    /* ====================== */


    public static void processSheet(final Sheet sheet, final Object processor) {
        // todo: add input parameter validation

        final ConversionInfo conversionInfo = new ConversionInfo();
        final Notifiers notifiers = new Notifiers();
        final Map<String, HeaderInfo> headerInfoMap = new LinkedHashMap<String, HeaderInfo>();

        findConversionIssueHandlerAnnotation(processor, conversionInfo);

        findColumnValueAnnotation(processor, headerInfoMap);
        findProcessHeaderAnnotation(processor, headerInfoMap);
        findProcessColumnAnnotation(processor, headerInfoMap);

        findPerformAnnotation(processor, notifiers);

        findBeforeTableProcessedAnnotation(processor, notifiers);
        findBeforeHeaderProcessedAnnotation(processor, notifiers);
        findBeforeRowProcessedAnnotation(processor, notifiers);
        findAfterTableProcessedAnnotation(processor, notifiers);
        findAfterHeaderProcessedAnnotation(processor, notifiers);
        findAfterRowProcessedAnnotation(processor, notifiers);

        findAfterNoTableFoundAnnotation(processor, notifiers);

        if (headerInfoMap.size() == 0) {
            throw new IllegalStateException("object contains no header definition. " +
                    "please annotate at least one field with ColumnValue annotation " +
                    "or at least one method with ProcessHeader or ProcessColumn annotation");
        }

        boolean tableFound = false;

        final Iterator<Row> rowIterator = sheet.rowIterator();
        while (rowIterator.hasNext()) {
            Row row = rowIterator.next();

            if (!tableFound) {
                boolean foundHeader = lookForHeader(row, headerInfoMap);

                if (foundHeader) {
                    notifyBeforeTableProcessed(notifiers, processor);
                    notifyBeforeHeaderProcessed(notifiers, processor);
                    processHeader(row, headerInfoMap, processor, conversionInfo);
                    notifyAfterHeaderProcessed(notifiers, processor);
                    tableFound = true;
                }
            } else {
                notifyBeforeRowProcessed(notifiers, processor);
                processRow(row, headerInfoMap, processor, conversionInfo);
                notifyAfterRowProcessed(notifiers, processor);
            }
        }

        if (tableFound) {
            notifyAfterTableProcessed(notifiers, processor);
        } else {
            notifyAfterNoTableFound(notifiers, processor);
        }
    }

    /* ====================== */
    /* --- helper methods --- */
    /* ====================== */


    /* ==================== */
    /* --- notification --- */
    /* ==================== */


    private static void notify(final Object processor, final Iterator<Method> methodIterator) {
        while (methodIterator.hasNext()) {
            Method method = methodIterator.next();

            ReflectionUtil.invokeMethod(processor, method);
        }
    }


    private static void notifyBeforeTableProcessed(final Notifiers notifiers, final Object processor) {
        notify(processor, notifiers.getMethodToCallBeforeTableProcessedIterator());
    }


    private static void notifyAfterTableProcessed(final Notifiers notifiers, final Object processor) {
        notify(processor, notifiers.getMethodToCallAfterTableProcessedIterator());
    }


    private static void notifyAfterNoTableFound(final Notifiers notifiers, final Object processor) {
        notify(processor, notifiers.getMethodToCallAfterNoTableFoundIterator());
    }


    private static void notifyBeforeHeaderProcessed(final Notifiers notifiers, final Object processor) {
        notify(processor, notifiers.getMethodToCallBeforeHeaderProcessedIterator());
    }


    private static void notifyAfterHeaderProcessed(final Notifiers notifiers, final Object processor) {
        notify(processor, notifiers.getMethodToCallAfterHeaderProcessedIterator());
    }


    private static void notifyBeforeRowProcessed(final Notifiers notifiers, final Object processor) {
        notify(processor, notifiers.getMethodToCallBeforeRowProcessedIterator());
    }


    private static void notifyAfterRowProcessed(final Notifiers notifiers, final Object processor) {
        notify(processor, notifiers.getMethodToCallAfterRowProcessedIterator());
    }


    /* ======================== */
    /* --- value conversion --- */
    /* ======================== */


    private static Object convertCellValue(final Cell cell, final Class<?> expectedValueClass) {
        Object value;

        // todo: add conversion for all numeric types
        // todo: test situation when number column contains null
        if (Date.class.equals(expectedValueClass)) {
            value = cell.getDateCellValue();
        } else if (Integer.class.equals(expectedValueClass) || Integer.TYPE.equals(expectedValueClass)) {
//            value = (int) cell.getNumericCellValue();
            value = PoiUtil.getIntegerCellValue(cell);
        } else if (Long.class.equals(expectedValueClass) || Long.TYPE.equals(expectedValueClass)) {
            value = (long) cell.getNumericCellValue();
        } else if (Float.class.equals(expectedValueClass) || Float.TYPE.equals(expectedValueClass)) {
            value = (float) cell.getNumericCellValue();
        } else if (Double.class.equals(expectedValueClass) || Double.TYPE.equals(expectedValueClass)) {
            value = cell.getNumericCellValue();
        } else if (BigDecimal.class.equals(expectedValueClass)) {
            value = new BigDecimal(cell.getNumericCellValue());
        } else if (Boolean.class.equals(expectedValueClass)) {
            value = PoiUtil.getBooleanCellValue(cell);
        } else if (Boolean.TYPE.equals(expectedValueClass)) {
            value = (boolean) PoiUtil.getBooleanCellValue(cell);
        } else {
            value = PoiUtil.getStringCellValue(cell);
        }

        return value;
    }


    private static Object convertCellValueWithIssueHandling(final Cell cell, final String headerText, final Class<?> expectedValueClass, final Object processor, final Method conversionIssueHandler) {
        Object value;

        if (conversionIssueHandler == null) {
            // todo: add some more specific message when there is an exception
            value = convertCellValue(cell, expectedValueClass);
        } else {
            try {
                value = convertCellValue(cell, expectedValueClass);
            } catch (Exception e) {
                value = ReflectionUtil.invokeMethod(processor, conversionIssueHandler, cell, headerText, expectedValueClass);
            }
        }

        return value;
    }


    /* ======================== */
    /* --- table processing --- */
    /* ======================== */


    private static boolean lookForHeader(final Row row, final Map<String, HeaderInfo> headerInfoMap) {
        Iterator<Cell> cellIterator = row.cellIterator();

        while (cellIterator.hasNext()) {

            Cell cell = cellIterator.next();

            String value = PoiUtil.getStringCellValue(cell);

            HeaderInfo headerInfo = headerInfoMap.get(value);

            if (headerInfo != null) {
                headerInfo.addHeaderIndex(cell.getColumnIndex());
            }
        }

        boolean isAtLeastOneFound = false;
        boolean areAllRequiredFound = true;

        for (String header : headerInfoMap.keySet()) {
            HeaderInfo headerInfo = headerInfoMap.get(header);

            if (headerInfo.hasHeaderIndexesSet()) {
                isAtLeastOneFound = true;
            } else if (headerInfo.isRequired()) {
                areAllRequiredFound = false;
                break;
            }
        }

        if (isAtLeastOneFound && !areAllRequiredFound) {
            for (String header : headerInfoMap.keySet()) {
                headerInfoMap.get(header).clearHeaderIndexes();
            }
        }

        return isAtLeastOneFound && areAllRequiredFound;
    }


    private static void processRow(final Row row, final Map<String, HeaderInfo> headerInfoMap, final Object processor, final ConversionInfo conversionInfo, final boolean isHeaderRow) {
        for (String headerText : headerInfoMap.keySet()) {
            HeaderInfo headerInfo = headerInfoMap.get(headerText);

            Iterator<Integer> indexIterator = headerInfo.getHeaderIndexesIterator();
            while (indexIterator.hasNext()) {
                Cell cell = row.getCell(indexIterator.next());

                Iterator<Method> methodIterator;

                if (!isHeaderRow) {
                    Iterator<Field> fieldIterator = headerInfo.getFieldToSetIterator();

                    if (fieldIterator.hasNext()) {

                        Object value;
                        while (fieldIterator.hasNext()) {
                            Field field = fieldIterator.next();

                            value = convertCellValueWithIssueHandling(cell, headerText, field.getType(), processor, conversionInfo.getConversionIssueHandler());

                            ReflectionUtil.setFieldValue(processor, field, value);
                        }
                    }

                    methodIterator = headerInfo.getMethodToCallOnRowColumn();

                } else {

                    methodIterator = headerInfo.getMethodToCallOnHeaderColumn();

                }

                while (methodIterator.hasNext()) {
                    Method method = methodIterator.next();

                    ReflectionUtil.invokeMethod(processor, method, cell);
                }
            }
        }
    }


    private static void processHeader(final Row row, final Map<String, HeaderInfo> headerInfoMap, final Object processor, final ConversionInfo conversionInfo) {
        processRow(row, headerInfoMap, processor, conversionInfo, true);
    }


    private static void processRow(final Row row, final Map<String, HeaderInfo> headerInfoMap, final Object processor, final ConversionInfo conversionInfo) {
        processRow(row, headerInfoMap, processor, conversionInfo, false);
    }


    /* ================== */
    /* --- validation --- */
    /* ================== */


    private static void validateConversionIssueHandlerMethod(final Method method) {
        if (!Object.class.equals(method.getReturnType())) {
            throw new IllegalStateException("only Object return type is allowed for method " + method);
        }

        Class<?>[] parameters = method.getParameterTypes();

        if (parameters.length != 3 ||
                !Cell.class.equals(parameters[0]) ||
                !String.class.equals(parameters[1]) ||
                !Class.class.equals(parameters[2])
                ) {
            throw new IllegalStateException("there must be exactly 3 input parameters: (" + Cell.class + " cell, String headerText, Class<?> expectedValueClass) for method " + method);
        }
    }


    private static void validateInputField(final Field field) {
        if (String.class.equals(field.getType()) ||
                Date.class.equals(field.getType()) ||
                Integer.class.equals(field.getType()) ||
                Integer.TYPE.equals(field.getType()) ||
                Long.class.equals(field.getType()) ||
                Long.TYPE.equals(field.getType()) ||
                Float.class.equals(field.getType()) ||
                Float.TYPE.equals(field.getType()) ||
                Double.class.equals(field.getType()) ||
                Double.TYPE.equals(field.getType()) ||
                BigDecimal.class.equals(field.getType()) ||
                Boolean.class.equals(field.getType()) ||
                Boolean.TYPE.equals(field.getType())
                ) {
            // do nothing
        } else {
            throw new IllegalStateException(
                    "one of this types:\n" +
                            "\tString, Date, int, Integer, long, Long, float, Float, double, Double, BigDecimal, boolean or Boolean\n" +
                            "must be used for field " + field);
        }
    }


    private static void validateInputMethod(final Method method) {
        Class<?>[] parameters = method.getParameterTypes();

        if (parameters.length != 1 || !Cell.class.equals(parameters[0])) {
            throw new IllegalStateException("only one argument of type " + Cell.class + " is allowed for input method " + method);
        }
    }


    private static void validateNotifiedMethod(final Method method) {
        if (method.getParameterTypes().length > 0) {
            throw new IllegalStateException("no arguments allowed for notification method " + method);
        }
    }


    /* ========================= */
    /* --- annotation search --- */
    /* ========================= */


    private static void findConversionIssueHandlerAnnotation(final Object processor, final ConversionInfo conversionInfo) {
        Map<Method, ConversionIssueHandler> methods = AnnotationUtil.findAnnotatedMethods(processor.getClass(), ConversionIssueHandler.class);

        if (methods.size() > 1) {
            throw new IllegalStateException("only one method may be annotated with the ConversionIssueHandler annotation");
        } else if (methods.size() == 1) {
            Method method = methods.keySet().iterator().next();
            validateConversionIssueHandlerMethod(method);
            conversionInfo.setConversionIssueHandler(method);
        }
    }


    private static <A extends Annotation> Set<Method> findAnnotatedMethods(final Object object, final Class<A> annotationClass) {
        return AnnotationUtil.findAnnotatedMethods(object.getClass(), annotationClass).keySet();
    }


    private static void findPerformAnnotation(final Object processor, final Notifiers notifiers) {
        Map<Method, Perform> methods = AnnotationUtil.findAnnotatedMethods(processor.getClass(), Perform.class);

        for (Method method : methods.keySet()) {
            validateNotifiedMethod(method);

            Perform perform = methods.get(method);

            if (When.AFTER.equals(perform.when())) {
                switch (perform.processed()) {
                    case TABLE:
                        notifiers.addMethodToCallAfterTableProcessed(method);
                        break;
                    case HEADER:
                        notifiers.addMethodToCallAfterHeaderProcessed(method);
                        break;
                    case ROW:
                        notifiers.addMethodToCallAfterRowProcessed(method);
                        break;
                }
            } else if (When.BEFORE.equals(perform.when())) {
                switch (perform.processed()) {
                    case TABLE:
                        notifiers.addMethodToCallBeforeTableProcessed(method);
                        break;
                    case HEADER:
                        notifiers.addMethodToCallBeforeHeaderProcessed(method);
                        break;
                    case ROW:
                        notifiers.addMethodToCallBeforeRowProcessed(method);
                        break;
                }
            }
        }
    }


    private static void findBeforeTableProcessedAnnotation(final Object processor, final Notifiers notifiers) {
        for (Method method : findAnnotatedMethods(processor, BeforeTableProcessed.class)) {
            validateNotifiedMethod(method);
            notifiers.addMethodToCallBeforeTableProcessed(method);
        }
    }


    private static void findAfterNoTableFoundAnnotation(final Object processor, final Notifiers notifiers) {
        for (Method method : findAnnotatedMethods(processor, AfterNoTableFound.class)) {
            validateNotifiedMethod(method);
            notifiers.addMethodToCallAfterNoTableFound(method);
        }
    }


    private static void findAfterTableProcessedAnnotation(final Object processor, final Notifiers notifiers) {
        for (Method method : findAnnotatedMethods(processor, AfterTableProcessed.class)) {
            validateNotifiedMethod(method);
            notifiers.addMethodToCallAfterTableProcessed(method);
        }
    }


    private static void findBeforeHeaderProcessedAnnotation(final Object processor, final Notifiers notifiers) {
        for (Method method : findAnnotatedMethods(processor, BeforeHeaderProcessed.class)) {
            validateNotifiedMethod(method);
            notifiers.addMethodToCallBeforeHeaderProcessed(method);
        }
    }


    private static void findAfterHeaderProcessedAnnotation(final Object processor, final Notifiers notifiers) {
        for (Method method : findAnnotatedMethods(processor, AfterHeaderProcessed.class)) {
            validateNotifiedMethod(method);
            notifiers.addMethodToCallAfterHeaderProcessed(method);
        }
    }


    private static void findBeforeRowProcessedAnnotation(final Object processor, final Notifiers notifiers) {
        for (Method method : findAnnotatedMethods(processor, BeforeRowProcessed.class)) {
            validateNotifiedMethod(method);
            notifiers.addMethodToCallBeforeRowProcessed(method);
        }
    }


    private static void findAfterRowProcessedAnnotation(final Object processor, final Notifiers notifiers) {
        for (Method method : findAnnotatedMethods(processor, AfterRowProcessed.class)) {
            validateNotifiedMethod(method);
            notifiers.addMethodToCallAfterRowProcessed(method);
        }
    }


    private static void findColumnValueAnnotation(final Object processor, final Map<String, HeaderInfo> headerInfoMap) {
        Map<Field, ColumnValue> fields = AnnotationUtil.findAnnotatedFields(processor.getClass(), ColumnValue.class);

        for (Field field : fields.keySet()) {
            validateInputField(field);

            ColumnValue columnValue = fields.get(field);

            HeaderInfo headerInfo = headerInfoMap.get(columnValue.headerText());
            if (headerInfo == null) {
                headerInfo = new HeaderInfo();
                headerInfoMap.put(columnValue.headerText(), headerInfo);
            }

            headerInfo.setRequired(columnValue.isRequired());
            headerInfo.addFieldToSet(field);
        }
    }


    private static void findProcessHeaderAnnotation(final Object processor, final Map<String, HeaderInfo> headerInfoMap) {
        Map<Method, ProcessHeader> methods = AnnotationUtil.findAnnotatedMethods(processor.getClass(), ProcessHeader.class);

        for (Method method : methods.keySet()) {
            validateInputMethod(method);

            ProcessHeader processHeader = methods.get(method);

            HeaderInfo headerInfo = headerInfoMap.get(processHeader.headerText());
            if (headerInfo == null) {
                headerInfo = new HeaderInfo();
                headerInfoMap.put(processHeader.headerText(), headerInfo);
            }

            headerInfo.setRequired(processHeader.isRequired());
            headerInfo.addMethodToCallOnHeaderColumn(method);
        }
    }


    private static void findProcessColumnAnnotation(final Object processor, final Map<String, HeaderInfo> headerInfoMap) {
        Map<Method, ProcessColumn> methods = AnnotationUtil.findAnnotatedMethods(processor.getClass(), ProcessColumn.class);

        for (Method method : methods.keySet()) {
            validateInputMethod(method);

            ProcessColumn processColumn = methods.get(method);

            HeaderInfo headerInfo = headerInfoMap.get(processColumn.headerText());
            if (headerInfo == null) {
                headerInfo = new HeaderInfo();
                headerInfoMap.put(processColumn.headerText(), headerInfo);
            }

            headerInfo.setRequired(processColumn.isRequired());
            headerInfo.addMethodToCallOnRowColumn(method);
        }
    }
}
